BGP ASNの番号からIPアドレス調査してAWS WAFでフィルターしてみた

BGP ASNの番号からIPアドレス調査してAWS WAFでフィルターしてみた

Clock Icon2024.09.30

こんにちは、コカコーラ大好きカジです。

お客様の要望により、BGP ASNの番号を基にIPアドレスを調査し、AWS WAFでフィルタリングを行う方法を試しました。ここでは、そのアーキテクチャと具体的な実装方法について詳しく解説します。

アーキテクチャの概要

Lambdaが定期的にBGPview APIからASNに関連するIP情報を取得し、AWS WAFのIPセットにその情報を反映する仕組みです。LambdaはCloudWatchによってスケジュールされ、結果はCloudWatch Logsに記録されます。このAWS WAFのIP setsは、CloudFrontやALBでのトラフィックフィルタリングに活用されます。

  1. 外部データソース(ASN情報取得サービス)

    外部サービス(BGPview API)を使用して、特定のASNに関連するIPプレフィックスを取得します。

  2. AWS Lambda

    Lambda関数が1日1回実行され、外部サービスからASNに基づくIPアドレス範囲を取得し、AWS WAFのIPセットを更新します。

    • ライブラリ requests を使用して外部APIからデータを取得。
    • boto3 を使用してAWS WAFのIPセットを取得し、更新します。
  3. AWS WAF (Web Application Firewall)

    WAFにはCloudFrontやApplication Load Balancerに対するフィルタリングを行うIPセットが設定されており、Lambda関数がこれを更新します。

    • IPセットは定期的に更新され、特定のASNに基づくIPアドレスブロックが追加されます。
  4. Amazon CloudWatch:(今回対象外)

    CloudWatchはLambdaのスケジュール実行を管理し、1日1回Lambda関数をトリガーします。また、Lambda関数の実行ログはCloudWatch Logsに記録されます。

構成図

asn-ip-set-waf

前提条件

  • 実際の運用を行なっていないため、考慮漏れなどが考えられます。導入にはご自分の環境で検証の上、ご利用お願いいたします。
  • CloudFrontに接続したAWS WAFとブロックリストを入れるIP Setを構築済みとします。
  • Python素人なので、IP情報が取れなかった場合の状況は考慮していません。

AWS CLIでIPsetのIDとARNを取得する

作成済みの、AWS WAFのIP SetsのIDやARNをAWS CLIで確認します。

    aws wafv2 list-ip-sets --scope CLOUDFRONT --region <region>
    {
"IPSets": [
    {
        "Name": "test-kaji-web-acl-Custom-ipaddress-blocklist",
        "Id": "6a0d328b-8e51-463b-af98-xxxxxxxxxxxxx",
        "Description": "",
        "LockToken": "236ab2bd-f8d8-4aa9-b756-xxxxxxxxxxxxx",
        "ARN": "arn:aws:wafv2:<region>:<account-id>:global/ipset/test-kaji-web-acl-Custom-ipaddress-blocklist/6a0d328b-8e51-463b-af98-xxxxxxxxxxxxx"
    }
]
    }

Lambdaのコード

import json
import boto3
import requests

# WAF IPセットの情報を設定
WAF_IP_SET_ID = 'your-ip-set-id'  # 作成したWAF IPセットのIDをここに記入
WAF_SCOPE = 'CLOUDFRONT'  # 'REGIONAL' または 'CLOUDFRONT' に変更

# BGPview APIで特定のASNからIPプレフィックスを取得
ASN = 'xxxxxx'  # フィルタリングしたいASNをここに記入

def lambda_handler(event, context):
    waf_client = boto3.client('wafv2')

		try:
		    # BGPview APIからデータ取得
		    response = requests.get(f'https://api.bgpview.io/asn/{ASN}/prefixes')
	  except requests.exceptions.HTTPError as http_err:
        return {
            'statusCode': 500,
            'body': json.dumps(f'HTTP error occurred: {http_err}')
        }
    except requests.exceptions.ConnectionError as conn_err:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Connection error occurred: {conn_err}')
        }
    except requests.exceptions.Timeout as timeout_err:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Timeout error occurred: {timeout_err}')
        }
    except requests.exceptions.RequestException as req_err:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Request error occurred: {req_err}')
        }

    prefixes_data = response.json()
    ip_ranges = []

    # プレフィックスデータからIPv4のIP範囲を抽出
    for prefix in prefixes_data['data']['ipv4_prefixes']:
        ip_ranges.append(prefix['prefix'])

		try:
		    # WAF IPセットの最新バージョンを取得, NameをWeb Aclのリストに修正
		    response = waf_client.get_ip_set(
		        Name='test-kaji-web-acl-Custom-ipaddress-blocklist',
		        Scope=WAF_SCOPE,
		        Id=WAF_IP_SET_ID
		    )

		    lock_token = response['LockToken']
		    current_ip_set = response['IPSet']['Addresses']

		    # IPセットを更新, NameをWeb Aclのリストに修正
		    response = waf_client.update_ip_set(
		        Name='test-kaji-web-acl-Custom-ipaddress-blocklist',
		        Scope=WAF_SCOPE,
		        Id=WAF_IP_SET_ID,
		        Addresses=ip_ranges,
		        LockToken=lock_token
		    )
    except boto3.exceptions.Boto3Error as boto_err:
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error occurred while get or updating WAF IP: {boto_err}')

    return {
        'statusCode': 200,
        'body': json.dumps('IP set updated successfully')
    }

LambdaのIAM Role

wafv2:GetIPSetは、既存のIPセットの情報を取得するための権限です。また、wafv2:UpdateIPSetは、IPセットに新しいIPアドレス範囲を追加するために必要な権限です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:<region>:<account-id>:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:<region>:<account-id>:log-group:/aws/lambda/<lambda-function-name>:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "wafv2:GetIPSet",
                "wafv2:UpdateIPSet",
                "wafv2:ListIPSets"
            ],
            "Resource": "arn:aws:wafv2:<region>:<account-id>:global/ipset/*"
        }
    ]
}

Python Requestライブラリの準備(Lambda Layerの作成)

CloudShellでRequestライブラリの準備し、S3へ保存します。

# pyenvの依存パッケージをインストールする:
sudo yum install -y gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel xz xz-devel libffi-devel git

# pyenvをインストールする:
curl https://pyenv.run | bash

# インストール後、以下のコマンドを実行して環境変数を設定します(.bashrcに追加します)。
export PATH="$HOME/.pyenv/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

# これを実行した後、次のようにして設定を反映します:
source ~/.bashrc

# pyenvでPython 3.12をインストールします。
pyenv install 3.12.0
pyenv global 3.12.0
python --version

# 必要なディレクトリを作成
mkdir -p lambda_layer/python/lib/python3.12/site-packages/

# requestsライブラリをインストール
pip install requests -t lambda_layer/python/lib/python3.12/site-packages/

# Lambda Layer用のファイルをZIPに圧縮
cd lambda_layer
zip -r9 ../layer_requests.zip .

# S3バケットがない場合は作成
aws s3 mb s3://your-bucket-name

# ZIPファイルをS3にアップロード
aws s3 cp ../layer_requests.zip s3://your-bucket-name/

Lambda Layerの適用

  1. AWS Lambdaコンソールに移動し、「レイヤーの作成」を選択します。
  2. 先ほどS3にアップロードしたrequestのZIPファイルを指定して、Lambda Layerを作成します。
  3. Python 3.12に対応するLayerとして作成します。

LambdaのIAM Role

以下のポリシーを「カスタムポリシー」として作成し、Lambda用のIAMロールにアタッチします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:<region>:<account-id>:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:<region>:<account-id>:log-group:/aws/lambda/asn-ip-list-add:*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "wafv2:GetIPSet",
                "wafv2:UpdateIPSet",
                "wafv2:ListIPSets"
            ],
            "Resource": "arn:aws:wafv2:<region>:<account-id>:global/ipset/*"
        }
    ]
}

試してみた

  • 簡単に試せる自分の利用している回線キャリアのBGP ASNを用いてカウントモードでお試しください
  • Lambdaをテスト実行するとIP setsにIPアドレスリストが追加され、カウントされることを確認します。

IP-set-WAF

まとめ

今回の実装では、まだエラーハンドリングに改善の余地があるかもしれませんが、基本的なフローは実現できています。より高度な実装としては、Lambda@EdgeやCloudFront Functionsを活用する方法も考えられますが、今回の方法はシンプルでスケーラブルと思ってます。
どなたかのお役に立てれば光栄です。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.